/*
 * Checkstyle4nb is a plugin for netbeans for checking Java coding conventions.
 * Copyright (C) 2006-2007, Andrea Matsunaga
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

package com.ammatsun.checkstyle4nb.actions;

import com.puppycrawl.tools.checkstyle.Checker;
import com.puppycrawl.tools.checkstyle.ConfigurationLoader;
import com.puppycrawl.tools.checkstyle.PropertiesExpander;
import com.puppycrawl.tools.checkstyle.api.CheckstyleException;

import org.openide.filesystems.FileUtil;
import org.openide.loaders.DataFolder;
import org.openide.loaders.DataObject;
import org.openide.nodes.Node;
import org.openide.util.HelpCtx;
import org.openide.util.NbBundle;
import org.openide.util.actions.CookieAction;
import org.openide.windows.IOProvider;
import org.openide.windows.InputOutput;
import org.openide.windows.OutputWriter;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Properties;

/**
 * Offers the functionality of checking the style of Java source
 * files using checkstyle 4.3.
 *
 * @version 1.1, 2007/Dec/11 - changes to fit netbeans 6.0<br/>
 * 1.0, 2006/Jul/12
 * @author     Andrea Matsunaga
 */
public final class Checkstyle extends CookieAction {
    /** Resource for the Sun Coding Convention configuration. */
    public static final String SUN_CONVENTIONS = "com/ammatsun/checkstyle4nb/actions/sun_checks.xml";

    /** Writer to output tab. */
    private OutputWriter writer = null;
    /** Checkstyle listener. */
    private NbListener nbListener = null;
    /** Style checker. */
    private Checker checker = null;
    /** List of files processed. */
    private ArrayList processed = null;

    /**
     * Actually perform the action, in this case invoke checkstyle to process
     * the selected files.
     * @param activatedNodes set of nodes selected when the action is invoked
     */
    protected void performAction(Node[] activatedNodes) {
        processed = new ArrayList();
        Node[] selectedNodes = uniqueNodes(activatedNodes);
        // no node selected
        if ((null == selectedNodes) || (0 == selectedNodes.length)) {
            return;
        }

        // "Checkstyle" output tab name defined
        String tabName = NbBundle.getMessage(Checkstyle.class, "CTL_Checkstyle");

        // "Checkstyle" tab is created in output window for writing the list
        // of errors found
        InputOutput io = IOProvider.getDefault().getIO(tabName, false);

        // Select Checkstyle tab ensuring it is visible
        io.select();
        writer = io.getOut();

        ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader();
        try {

            // Clean the output window
            writer.reset();

            // Retrieve current checkstyle settings
            CheckstyleSettings settings = new CheckstyleSettings();
            String configName = (String) settings.get(CheckstyleSettings.CHECKSTYLE_CONFIG);
            String propName = (String) settings.get(CheckstyleSettings.CHECKSTYLE_PROPERTIES);
            Thread.currentThread().setContextClassLoader(Checkstyle.class.getClassLoader());

            // Create a new Checker
            checker = new Checker();

            // Load extra properties for configuring checkstyle if the value is set
            Properties prop = new Properties();

            if ((null != propName) && (0 != propName.length())) {
                prop.load(new FileInputStream(propName));
            }

            // If the value for checkstyle configuration file does not exist,
            // default to Sun conventions configuration.
            InputStream is = null;

            if ((null == configName) || (0 == configName.length())) {
                is = Checkstyle.class.getClassLoader().getResourceAsStream(SUN_CONVENTIONS);

                if (null != is) {
                    writer.println("No checkstyle configuration, using Sun Conventions.");
                    writer.println("With properties: [" + propName + "]");
                    checker.configure(ConfigurationLoader.loadConfiguration(is,
                            new PropertiesExpander(prop), true));
                    is.close();
                } else {
                    // This case should really not happen as Sun Coding Conventions
                    // should be part of this module
                    writer.println("No checkstyle configuration and failed to load Sun Conventions.");

                    return;
                }
            } else {
                // Use the defined configuration and properties file.
                writer.println("Using configuration: [" + configName + "]");
                writer.println("With properties: [" + propName + "]");
                checker.configure(ConfigurationLoader.loadConfiguration(configName,
                        new PropertiesExpander(prop)));
            }

            nbListener = new NbListener(writer, selectedNodes);
            checker.addListener(nbListener);

            // Go through all selected file nodes and process checkstyle
            for (int i = 0; i < selectedNodes.length; i++) {
                processNodeCheck(selectedNodes[i], i);
            }
        } catch (CheckstyleException e) {
            writer.println("Checkstyle Error: " + e.getMessage());
        } catch (IOException e) {
            writer.println("Checkstyle Output Error: " + e.getMessage());
        } finally {
            writer.flush();
            writer.close();
            Thread.currentThread().setContextClassLoader(originalClassLoader);
        }
    }

    /**
     * Actually checks the node for coding conventions.
     * @param activatedNode a Node that the user selected
     * @param id the index of this Node in the array
     */
    private void processNodeCheck(Node activatedNode, int id) {
        // Set the id to be able to define which file generated the message
        // so that the user can be redirected to the exact line when message
        // is clicked
        nbListener.setCurrId(id);
        DataObject dataObj = activatedNode.getLookup().lookup(DataObject.class);
        File myFile = FileUtil.toFile(dataObj.getPrimaryFile());
        // Process the file
        writer.println("Analyzing: " + myFile.getAbsolutePath());
        checker.process(new File[] {myFile});
    }

    /**
     * Gets the list of unique nodes when both java files and its parent folder
     * have been selected.
     * @param activatedNodes list of activated nodes by the user
     * @return list of unique selected nodes (non duplicates)
     */
    private Node[] uniqueNodes(Node[] activatedNodes) {
        // no node selected
        if ((null == activatedNodes) || (0 == activatedNodes.length)) {
            return null;
        }
        ArrayList<Node> toProcess = new ArrayList<Node>();
        for (int i = 0; i < activatedNodes.length; i++) {
            DataFolder dataFdr = activatedNodes[i].getLookup().lookup(DataFolder.class);
            if (null != dataFdr) {
                //Get the children of the data folder recursively:
                Enumeration children = dataFdr.children(true);
                while (children.hasMoreElements()) {
                    DataObject dataObj = (DataObject) children.nextElement();
                    if (dataObj.getPrimaryFile().getMIMEType().equals("text/x-java")) {
                        Node child = dataObj.getNodeDelegate();
                        if (!toProcess.contains(child)) {
                            toProcess.add(child);
                        }
                    }
                }
            } else {
                if (!toProcess.contains(activatedNodes[i])) {
                    toProcess.add(activatedNodes[i]);
                }
            }
        }
        return toProcess.toArray(new Node[toProcess.size()]);
    }

    /**
     * Enables action only if selected nodes are java files or folders that
     * contain java files.
     * @param activatedNodes list of nodes the user selected
     * @return true if folder should be enabled
     */
    @Override
    protected boolean enable(Node[] activatedNodes) {
        if (super.enable(activatedNodes)) {
            // Looking at the activated nodes
            for (int i = 0; i < activatedNodes.length; i++) {
                // Look up the node's data folder:
                DataFolder dFo = activatedNodes[i].getLookup().lookup(DataFolder.class);
                if (!(dFo == null)) {
                    // Get the children of the data folder:
                    DataObject[] objs = dFo.getChildren();
                    // If there is at least one Java file, return true
                    for (int j = 0; j < objs.length; j++) {
                        DataObject dataObj = objs[j];
                        if (dataObj.getPrimaryFile().getMIMEType().equals("text/x-java")) {
                            return true;
                        }
                    }
                } else {
                    // If at least one selected node is a Java file, return true
                    DataObject dataObj = activatedNodes[i].getLookup().lookup(DataObject.class);
                    if (dataObj.getPrimaryFile().getMIMEType().equals("text/x-java")) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

    /**
     * Get the mode of the action: how strict it should be about cookie support.
     * In this case action will be enabled if there are one or more selected
     * nodes and all of them support the given cookies.
     * @return the mode of the action (MODE_ALL).
     */
    protected int mode() {
        return CookieAction.MODE_ALL;
    }

    /**
     * Get a human presentable name of the checkstyle action.
     * @return checkstyle action name
     */
    public String getName() {
        return NbBundle.getMessage(Checkstyle.class, "CTL_Checkstyle");
    }

    /**
     * Get the cookies that this action requires. The cookies are disjunctive,
     * i.e. a node must support AT LEAST ONE of the cookies specified by this
     * method.
     * @return list with open cookie
     */
    protected Class[] cookieClasses() {
        return new Class[] {DataObject.class};
    }

    /**
     * Get the resource name for the icon.
     * @return the resource name for the icon
     */
    @Override
    protected String iconResource() {
        return "com/ammatsun/checkstyle4nb/actions/checkstyle.gif";
    }

    /**
     * Get current help context associated with this action.
     * @return current help context
     */
    public HelpCtx getHelpCtx() {
        return HelpCtx.DEFAULT_HELP;
    }

    /**
     * Indicates that this action should be performed synchronously as called
     * in the event thread.
     * @return false to indicate that action should be performed synchronously
     */
    @Override
    protected boolean asynchronous() {
        return false;
    }
}
